/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network.stateelements;

import com.crunch.network.StateElement;
import com.crunch.network.util.BitStreamReader;
import com.crunch.network.util.BitStreamWriter;
import com.crunch.network.util.Compressor;

import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;

/**
 * A {@link com.crunch.network.StateElement} containing a float.
 */
public class FloatElement extends StateElement {
	/**
	 * The mode of compression to use.
	 */
	enum CompressionMode {
		/**
		 * The float is transferred in full precision.
		 */
		NONE,

		/**
		 * The float is quantized using a fixed number of bits to represent the range {@code [0,1]}.
		 */
		UNSIGNED_NORMALIZED,

		/**
		 * The float is quantized using a fixed number of bits to represent the range {@code [-1,1]}.
		 */
		SIGNED_NORMALIZED,

		/**
		 * The float is quantized using a fixed number of bits to represent the range {@code [0,1)}.
		 */
		WRAPPED_NORMALIZED
	}

	/**
	 * Constructs the element to use no compression.
	 *
	 * @param   networkTransmissionMode the mode of network transmission to use for this element.
	 */
	public FloatElement(NetworkTransmissionMode networkTransmissionMode) {
		this(networkTransmissionMode, CompressionMode.NONE, 32);
	}

	/**
	 * Constructs the element to use the given type of compression.
	 *
	 * @param   networkTransmissionMode the mode of network transmission to use for this element.
	 * @param   compressionMode the mode of compression to use.
	 * @param   bitCount the number of bits to use.
	 */
	public FloatElement(NetworkTransmissionMode networkTransmissionMode,
	                    CompressionMode compressionMode, int bitCount) {
		super(networkTransmissionMode);

		if (bitCount <= 0 || bitCount > 32 || (compressionMode == CompressionMode.NONE && bitCount != 32)) {
			throw new IllegalArgumentException("Invalid bit count");
		}

		this.value = 0.0f;
		this.compressionMode = compressionMode;
		this.bitCount = bitCount;
	}

	/**
	 * Assigns a new value, dirtying the element if the value has changed.
	 *
	 * @param   value the new value.
	 */
	public void setValue(float value) {
		if (compressionMode == CompressionMode.NONE) {
			if (value != this.value) {
				setDirty();
			}
		} else {
			int oldCompressedValue = getCompressedValue(this.value);
			int newCompressedValue = getCompressedValue(value);
			if (newCompressedValue != oldCompressedValue) {
				setDirty();
			}
		}

		this.value = value;
	}

	/**
	 * Returns the element's value.
	 *
	 * @return  the element's value.
	 */
	public float getValue() {
		return value;
	}

	/**
	 * Reads the element from the stream.
	 *
	 * @param   reader the stream from which element data should be read.
	 * @throws  BufferUnderflowException if too little data is available for reading.
	 * @throws  IOException if an error in the data is detected.
	 */
	@Override
	public void read(BitStreamReader reader)
			throws BufferUnderflowException, IOException {
		switch (compressionMode) {
			case NONE:
				value = reader.readFloat();
				break;
			case UNSIGNED_NORMALIZED:
				value = reader.readUNormFloat(bitCount);
				break;
			case SIGNED_NORMALIZED:
				value = reader.readSNormFloat(bitCount);
				break;
			case WRAPPED_NORMALIZED:
				value = reader.readWrappedNormFloat(bitCount);
				break;
			default:
				assert false;
		}
	}

	/**
	 * Writes the element to the stream.
	 *
	 * @param   writer the stream to which element data should be written.
	 * @throws  BufferOverflowException if too much data is available for writing.
	 */
	@Override
	public void write(BitStreamWriter writer)
			throws BufferOverflowException {
		switch (compressionMode) {
			case NONE:
				writer.writeFloat(value);
				break;
			case UNSIGNED_NORMALIZED:
				writer.writeUNormFloat(value, bitCount);
				break;
			case SIGNED_NORMALIZED:
				writer.writeSNormFloat(value, bitCount);
				break;
			case WRAPPED_NORMALIZED:
				writer.writeWrappedNormFloat(value, bitCount);
				break;
			default:
				assert false;
		}
	}

	private int getCompressedValue(float val) {
		switch (compressionMode) {
			case UNSIGNED_NORMALIZED:
				return Compressor.compressUNormFloat(val, bitCount);
			case SIGNED_NORMALIZED:
				return Compressor.compressSNormFloat(val, bitCount);
			case WRAPPED_NORMALIZED:
				return Compressor.compressWrappedNormFloat(val, bitCount);
			default:
				assert false;
				return 0;
		}
	}

	private float value;
	private CompressionMode compressionMode;
	private int bitCount;
}
